﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Collada141;
using OptixDotNet;
using OptixDotNet.Math;
using RayDen.Library.Components;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Data.Imaging;
using RayDen.Library.Entity;
using RayDen.Library.Entity.Frames;
using RayDen.Library.Entity.Scene;
using RayDen.Library.Entity.SceneGraph;
using RayDen.Optix.Engine.Components;
using RayDen.Optix.Engine.Core;
using RayDen.Optix.Engine.Managers;
using RayDen.Optix.Engine.Scene.Factories;
using Buffer = OptixDotNet.Buffer;

namespace RayDen.Optix.Engine.Scene.Builders
{
    public class SceneBuilder : ISceneBuilder
    {

        public Buffer VertexBuffer;
        public Buffer NormalBuffer;
        public Buffer TexCoordBuffer;



        public OptixKernel IntersectionKernel;
        public OptixKernel BoundingBoxKernel;


        public List<MaterialInfo> Materials;
        public List<CameraInfo> Cameras;
        public List<LightsourceInfo> LightInfos;
        public List<BaseTriangleMeshInfo> Meshes;
        public List<TriangleDataInfo> Triangles;
        public SceneGeometryInfo SceneGeo;

        public SceneGraphService SGService;

        public RayDen.Optix.Engine.Core.EnvironmentMapInfo EnvMap;

        public TexManager texManager;


        public List<TriangleLight> TriLights;


        private List<INodeVisitor> visitors;

        internal Context OptixContext;

        public SceneBuilder()
        {
            IntersectionKernel = new OptixKernel
            {
                KernelPath = @"G:\Git\RayDen\Engines\RayDen.Optix.Engine\Scripts\triangle_mesh.cu.ptx",
                ProgramName = "mesh_intersect",
            };
            BoundingBoxKernel = new OptixKernel
            {
                KernelPath = @"G:\Git\RayDen\Engines\RayDen.Optix.Engine\Scripts\triangle_mesh.cu.ptx",
                ProgramName = "mesh_bounds",
            };
            Triangles = new List<TriangleDataInfo>();
            Materials = new List<MaterialInfo>();
            Cameras = new List<CameraInfo>();
            LightInfos = new List<LightsourceInfo>();
            Meshes = new List<BaseTriangleMeshInfo>();
            visitors = new List<INodeVisitor>
                {
                    CreateVisitor<MaterialsBuilder>(this),
                    CreateVisitor<LightBuilder>(this),
                    CreateVisitor<CameraBuilder>(this),
                    CreateVisitor<GeoDataBuilder>(this),
                    CreateVisitor<VolumeBuilder>(this),
                };

            texManager = EngineContext.Instance.TexManager;
        }


        public SceneInfo OpenFrame(FrameDescription frame)
        {
            var scene = new SceneInfo();

            if (SGService == null)
                SGService = new SceneGraphService();
            SGService.OpenFrame(frame);
            var sceneGraph = SGService.SceneGraph;
            visitors.ForEach(item => ((DependentComponentBase)item).Populate(sceneGraph));
            foreach (var nodeVisitor in visitors)
            {
                sceneGraph.Visit(nodeVisitor);
            }

            var materialProvider = new MaterialInfoContainer(new MaterialInfo(), this.Materials);
            scene.MaterialProvider = materialProvider;

            var frameMaterials = frame.Elements.Where(item => item is FrameMaterialInfo);
            var materials = frameMaterials as IFrameElement[] ?? frameMaterials.ToArray();
            //if (materials.Any())
            //{
            //    FrameMaterialBuilder = new FrameMaterialBuilder(materials.Cast<FrameMaterialInfo>().ToArray());
            //    this.MaterialInfos.AddRange(FrameMaterialBuilder.GetMaterialInfos());
            //}

            this.LoadTextures(this.Materials);


            Tracer.TraceLine("Total textures memory : {0:F5} Mbytes", ImageFactory.MemoryOccupied / (1024.0f * 1024.0f));



            //new DefaultMaterialContainer(new MaterialInfo(), this.MaterialInfos);
            /*
            scene.AreaLightAsMesh = areaLightsAsMesh;
            scene.Lights = this.Lights.Values.ToArray();
            scene.sceneData = new SceneDataInfo(this.SceneGeo);
            foreach (var triangleMeshInfo in scene.sceneData.Meshes)
            {
                triangleMeshInfo.MaterialID = materialProvider.GetMaterialID(triangleMeshInfo.MaterialName);
            }*/

            Tracer.TraceLine("Create Materials");
            scene.SceneGeometry = SceneGeo;
            scene.BBox = new BoundingBox(SceneGeo.BoundingBox().Min.ToVec3(),SceneGeo.BoundingBox().Max.ToVec3());
            // /--------------------Materials
            var mats = this.Materials.Select(materialInfo => EngineContext.Instance.MaterialFactory.CreateMaterial(materialInfo)).ToArray();
            foreach (var optixMaterialInfo in mats)
            {
                if (optixMaterialInfo.Info.Diffuse != null &&
                    texManager[optixMaterialInfo.Info.DiffuseTexture.Name] != null)
                {
                    optixMaterialInfo.Node["diffuse_map"].Set(texManager[optixMaterialInfo.Info.DiffuseTexture.Name]);
                    optixMaterialInfo.Node["use_texture"].Set(1);
                }
                else
                {
                    optixMaterialInfo.Node["diffuse_map"].Set(texManager.DefaultTexture);
                }
                optixMaterialInfo.Node["diffuse_color"].SetFloat3(RgbSpectrum.Max(optixMaterialInfo.Info.Kd, optixMaterialInfo.Info.Ks));
            }
            var dm = mats.First();
            Tracer.TraceLine("Create Optix scene nodes");

            var nf = new OptixNodeFactory(this.OptixContext);
            nf.Builder = AccelBuilder.Sbvh;
            nf.Traverser = AccelTraverser.Bvh;


            //create an acceleration structure for the geometry
            var accel = new Acceleration(OptixContext, nf.Builder, nf.Traverser);
            accel.VertexBufferName = "vertex_buffer";
            accel.IndexBufferName = "vindex_buffer";
            scene.TopObject = new GeometryGroup(this.OptixContext) { Acceleration = accel };
            this.BuildLightsources();
           

            //now attach the instance and accel to the geometry group
            scene.TopObject.Acceleration = accel;
            scene.TopObject.AddChildren(nf.CreateGeometry(dm, mats, this));

            var lights = this.TriLights;
            var desc = new BufferDesc()
            {
                Width = (ulong)(lights.Count()*2), 
                Format = Format.User, 
                Type = BufferType.Input, 
                ElemSize = (ulong)Marshal.SizeOf(typeof(TriangleLight))
            };
            var lightBuffer = EngineContext.Instance.ResourceManager.CreateBuffer(OptixContext, desc);
            {
                var stream = lightBuffer.Map();
                {
                    foreach (var triangleLight in lights)
                    {
                        triangleLight.normal.Normalize();
                        stream.Write(triangleLight);
                    }
                }
                lightBuffer.Unmap();

                OptixContext["lights"].Set(lightBuffer);
            }

            /*
            scene.BuildSceneData();
            scene.VolumeMats = this.VolumeMats;
            scene.texturesMap = texturesMap;
            scene.EnvironmentMap = EnvironmentMap;
            scene.OptTextures = this.Textures;*/
            width = frame.Get<int>("ImageWidth");
            height = frame.Get<int>("ImageHeight");
            scene.Cameras = this.Cameras.ToArray();
            scene.EnvMap = this.EnvMap;
            /*scene.Camera = Cameras.First().Pinhole ?
                (BaseCamera)new BasicPerspectiveCamera((Point)Cameras.First().Position, Cameras.First().Direction, Cameras.First().Up, width, height, Cameras.First().Fov)
                : new FiniteAppertureCamera((Point)Cameras.First().Position, Cameras.First().Direction, Cameras.First().Up, width, height, Cameras.First().Fov) { lensRadius = Cameras.First().LensRadius };*/
            //if (GlobalConfiguration.Instance.UseSceneCaching)
            //{
            //    var cacheFileName = string.Format("{0}{1}.cache", GlobalConfiguration.Instance.SceneCacheDirectory, frame.FrameName);
            //    if (File.Exists(cacheFileName))
            //    {
            //        scene.Cache = SceneCache.Load(cacheFileName);
            //    }
            //    else
            //    {

            //        Tracer.TraceLine("Building bvh cache");
            //        var da = new BvhDataAdapter(scene);
            //        var cache = new SceneCache { BvhData = da.BuildData() };
            //        cache.Save(cacheFileName);
            //        scene.Cache = cache;
            //    }
            //}
            return scene;
        }

        private void BuildLightsources()
        {
            TriLights = new List<TriangleLight>();
            foreach (var lightsourceInfo in this.LightInfos)
            {
                switch (lightsourceInfo.Type)
                {
                    case LightSourceTypes.Area:
                        var lm =
                            this.Meshes.FirstOrDefault(
                                m =>
                                    m.MeshName.Equals(lightsourceInfo.MeshName,
                                        StringComparison.InvariantCultureIgnoreCase));
                        if (lm != null)
                        {
                            this.TriLights.AddRange(this.Triangles.GetRange(lm.StartTriangle, lm.TrianglesCount).Select((lt,i)=>new TriangleLight()
                            {
                                v1 = SceneGeo.Vertices[lt.v0.VertexIndex].ToVec(),
                                v2 = SceneGeo.Vertices[lt.v1.VertexIndex].ToVec(),
                                v3 = SceneGeo.Vertices[lt.v2.VertexIndex].ToVec(),
                                normal = lt.ComputeVerticeNormal(0, SceneGeo.Vertices).ToVec(),
                                lgt_index = i,
                                emission = new Vector(1f)
                            }));
                            
                        }
                        break;
                    case LightSourceTypes.EnvironmentMap:
                        EnvMap = new EnvironmentMapInfo
                        {
                            EnvironmentMap = EngineContext.Instance.TexManager.CreateTextureSampler(lightsourceInfo.ImageName)
                        };
                        Tracer.TraceLine("Environment map initialized");

                        break;
                    case LightSourceTypes.Point:
                        break;
                }
            }
        }

        private void LoadTextures(List<MaterialInfo> materials)
        {
            foreach (var materialInfo in materials)
            {
                foreach (var textureInfo in new[] { materialInfo.BumpMap, materialInfo.AlphaTexture, materialInfo.Diffuse, materialInfo.NormalMap, materialInfo.Specular })
                {
                    var fullPath = GetTexture(textureInfo);
                    if (!string.IsNullOrWhiteSpace(fullPath))
                    {
                        texManager.LoadTexture(fullPath);
                    }
                }
            }
        }

        private string GetTexture(TextureInfo ti)
        {
            if (ti == null || !(ti is ImageTextureInfo))
                return null;
            return (ti as ImageTextureInfo).FilePath;
        }

        private TVisitor CreateVisitor<TVisitor>(params object[] deps)
           where TVisitor : SceneBuilderComponentBase, INodeVisitor, ISceneBuilder, new()
        {
            var vis = new TVisitor();
            vis.Populate(deps);
            vis.Builder = this;
            return vis;
        }


        public void Add<TSceneElement>(TSceneElement item) where TSceneElement : class
        {

        }

        public int width { get; set; }

        public int height { get; set; }
    }
}
